package com.atghost.miniofile.util;

import com.alibaba.fastjson.JSONObject;
import com.amazonaws.util.IOUtils;
import com.atghost.miniofile.config.MinioObject;
import com.atghost.miniofile.dto.req.FileBo;
import com.atghost.miniofile.dto.res.CommBo;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.minio.StatObjectResponse;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @author Admin
 * @version 1.0
 * @date 2024-5-19 14:36
 **/
@Component
public class FileBlh {
    private static Log logger = LogFactory.getLog(FileBlh.class);

    private static final String OBJECT_INFO_LIST = "minio.file.objects";
    private static final String MD5_KEY = "minio.file.md5s";
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

    @Value("${minio-bucket-name:condor}")
    private String bucketName;
    @Resource
    private MinioFileUtil minioFileUtil;
    @Resource
    private RedisTemplate redisTemplate;
    @Autowired
    private ObjectMapper objectMapper;

    /**
     * @description 上传单个文件
     */
    public void uploadFile(FileBo bo) {
        InputStream is = null;
        try {
            MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) bo.getReq();
            MultipartFile file = multipartRequest.getFile("file");
            is = file.getInputStream();
            String uuid = UUID.randomUUID().toString().replace("-", "");
            String dir = dateFormat.format(new Date());
            minioFileUtil.uploadObject(is, bucketName, dir, uuid + "-" + file.getOriginalFilename());
            bo.setFileName(dir + "/" + uuid + "-" + file.getOriginalFilename());
            CommBo.setSuccessBo(bo);
        } catch (Exception e) {
            CommBo.setFailBo(bo, e);
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException eis) {
            }
        }
    }

    /**
     * @description 获取文件路径
     */
    public void getFileUrl(FileBo bo) {
        try {
            String url = minioFileUtil.getObjectUrl(bucketName, bo.getFileName(), 100);
            logger.info("…………………………………………获取文件路径……………………………………" + url);
            bo.setFileUrl(url);
            CommBo.setSuccessBo(bo);
        } catch (Exception e) {
            CommBo.setFailBo(bo, e);
        }
    }

    /**
     * @description 删除文件
     */
    public void deleteFile(FileBo bo) {
        try {
            minioFileUtil.removeObject(bucketName, bo.getFileName());
            CommBo.setSuccessBo(bo);
        } catch (Exception e) {
            CommBo.setFailBo(bo, e);
        }
    }

    /**
     * @description 获取文件分片下载信息
     */
    public void getSplitFileInfo(FileBo bo) {
        try {
            StatObjectResponse objectInfo = minioFileUtil.getObjectInfo(bucketName, bo.getFileName());
            bo.setShardCount((int) Math.ceil((double) objectInfo.size() / (1024 * 1024 * 5)));
            CommBo.setSuccessBo(bo);
        } catch (Exception e) {
            CommBo.setFailBo(bo, e);
        }
    }

    /**
     * @description 文件分片下载
     */
    public void downSplitFile(FileBo bo) {
        try {
            StatObjectResponse objectInfo = minioFileUtil.getObjectInfo(bucketName, bo.getFileName());
            long fileSize = objectInfo.size();
            long startPos = (bo.getShardCount() - 1) * (1024 * 1024 * 5);
            long endPos = bo.getShardCount() * (1024 * 1024 * 5);
            if (endPos > fileSize) {
                endPos = fileSize;
            }
            long rangLength = endPos - startPos;
            bo.getRes().addHeader("Content-Type", "*/*");
            BufferedOutputStream bos = new BufferedOutputStream(bo.getRes().getOutputStream());
            logger.info("…………………bucketName……………" + bucketName);
            BufferedInputStream bis = new BufferedInputStream(
                    minioFileUtil.getObject(bucketName, bo.getFileName(), startPos, rangLength));
            IOUtils.copy(bis, bos);
        } catch (Exception e) {
            CommBo.setFailBo(bo, e);
        }
    }

    /**
     * @description 根据文件大小和文件的md5校验文件是否存在
     */
    public void checkSplitFile(FileBo bo) {
        try {
            if (ObjectUtils.isEmpty(bo.getMd5())) {
                bo.setCode(204);
                return;
            }
            String url = (String) redisTemplate.boundHashOps(MD5_KEY).get(bo.getMd5());
            if (ObjectUtils.isEmpty(url)) {
                bo.setCode(204); // 文件不存在
                return;
            }
            bo.setCode(200);
            bo.setFileUrl(url);
        } catch (Exception e) {
            CommBo.setFailBo(bo, e);
        }
    }

    /**
     * @description 文件上传，适合大文件，集成了分片上传
     */
    public void uploadSplitFile(FileBo bo) {
        try {
            MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) bo.getReq();
            MultipartFile file = multipartRequest.getFile("data");
            if (file == null) {
                bo.setCode(203);
                return;
            }
            int index = Integer.parseInt(multipartRequest.getParameter("index")); // 第几片
            int total = Integer.parseInt(multipartRequest.getParameter("total")); // 总片数
            String fileName = multipartRequest.getParameter("name");
            String md5 = multipartRequest.getParameter("md5");
            minioFileUtil.makeBucket(md5);
            String objectName = String.valueOf(index);
            if (index < total) {
                try {
                    logger.info("上传文件: " + md5 + " " + objectName);
                    minioFileUtil.putChunkObject(file.getInputStream(), md5, objectName); // 上传文件
                    bo.setCode(201); // 不是最后一片, 状态码为201
                } catch (Exception e) {
                    logger.error(e.getMessage());
                    bo.setCode(203);
                }
            } else {
                try {
                    minioFileUtil.putChunkObject(file.getInputStream(), md5, objectName);
                    bo.setCode(202); // 最后一片, 状态码为202
                    bo.setFileName(objectName);
                } catch (Exception e) {
                    logger.error(e.getMessage());
                    bo.setCode(203);
                }
            }
        } catch (Exception e) {
            CommBo.setFailBo(bo, e);
        }
    }

    /**
     * @description 文件合并
     */
    public void mergeSplitFile(FileBo bo) {
        logger.info("分片总数: " + bo.getShardCount());
        Map<String, Object> retMap = new HashMap<>();
        try {
            List<String> objectNameList = minioFileUtil.listObjectNames(bo.getMd5());
            if (bo.getShardCount() != objectNameList.size()) {
                bo.setCode(203);
            } else {
                // 开始合并请求
                String filenameExtension = StringUtils.getFilenameExtension(bo.getFileName());
                String uuid = UUID.randomUUID().toString();
                String dir = dateFormat.format(new Date());
                String objectName = dir + "/" + uuid + "-" + bo.getFileName();
                minioFileUtil.composeObject(bo.getMd5(), bucketName, objectName);
                // 合并成功之后删除对应的临时桶
                minioFileUtil.removeBucket(bo.getMd5());
                logger.info("创建文件 " + objectName + " ,删除桶 " + bo.getMd5() + " 成功");
                // 计算文件的md5
                String fileMd5 = null;
                try (InputStream inputStream = minioFileUtil.getObject(bucketName, objectName)) {
                    fileMd5 = FileMd5Util.calculateMd5(inputStream);
                } catch (IOException e) {
                    logger.error(e.getMessage());
                }
                // 计算文件真实的类型
                String type = null;
                if (!ObjectUtils.isEmpty(fileMd5) && fileMd5.equalsIgnoreCase(bo.getMd5())) {
                    String url = minioFileUtil.getObjectUrl(bucketName, objectName, 100);
                    redisTemplate.boundHashOps(MD5_KEY).put(fileMd5, objectName);
                    bo.setCode(200);
                } else {
                    minioFileUtil.removeObject(bucketName, objectName);
                    redisTemplate.boundHashOps(MD5_KEY).delete(fileMd5);
                    bo.setCode(203);
                }
                bo.setFileName(objectName);
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            bo.setCode(203);
        }
    }

    /**
     * @description 文件播放
     */
    public void videoPlay(FileBo bo) {
        logger.info("播放视频: " + bo.getFileName());
        // 设置响应报头
        String key = bucketName + "." + bo.getFileName();
        Object obj = redisTemplate.boundHashOps(OBJECT_INFO_LIST).get(key);
        // 记录视频文件的元数据
        MinioObject minioObject;
        if (obj == null) {
            StatObjectResponse objectInfo = null;
            try {
                objectInfo = minioFileUtil.getObjectInfo(bucketName, bo.getFileName());
            } catch (Exception e) {
                bo.getRes().setCharacterEncoding(StandardCharsets.UTF_8.toString());
                bo.getRes().setContentType("application/json;charset=utf-8");
                bo.getRes().setStatus(HttpServletResponse.SC_NOT_FOUND);
                try {
                    JSONObject json = new JSONObject();
                    json.put("operateSuccess", false);
                    bo.getRes().getWriter().write(objectMapper.writeValueAsString(json));
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
                return;
            }
            minioObject = new MinioObject();
            //BeanUtils.copyProperties(objectInfo, minioObject);
            minioObject.setBucket(objectInfo.bucket());
            minioObject.setRegion(objectInfo.region());
            minioObject.setObject(objectInfo.object());
            minioObject.setEtag(objectInfo.etag());
            minioObject.setSize(objectInfo.size());
            minioObject.setUserMetadata(objectInfo.userMetadata());
            redisTemplate.boundHashOps(OBJECT_INFO_LIST).put(key, minioObject);
        } else {
            minioObject = (MinioObject) obj;
        }
        // 获取文件的长度
        long fileSize = minioObject.getSize();
        // Accept-Ranges: bytes
        bo.getRes().setHeader("Accept-Ranges", "bytes");
        // pos开始读取位置;  last最后读取位置
        long startPos = 0;
        long endPos = fileSize - 1;
        String rangeHeader = bo.getReq().getHeader("Range");
        if (!ObjectUtils.isEmpty(rangeHeader) && rangeHeader.startsWith("bytes=")) {
            try {
                String numRang = bo.getReq().getHeader("Range").replaceAll("bytes=", "");
                if (numRang.startsWith("-")) {
                    endPos = fileSize - 1;
                    startPos = endPos - Long.parseLong(new String(numRang.getBytes(StandardCharsets.UTF_8), 1,
                            numRang.length() - 1)) + 1;
                } else if (numRang.endsWith("-")) {
                    endPos = fileSize - 1;
                    startPos = Long.parseLong(new String(numRang.getBytes(StandardCharsets.UTF_8), 0,
                            numRang.length() - 1));
                } else {
                    String[] strRange = numRang.split("-");
                    if (strRange.length == 2) {
                        startPos = Long.parseLong(strRange[0].trim());
                        endPos = Long.parseLong(strRange[1].trim());
                    } else {
                        startPos = Long.parseLong(numRang.replaceAll("-", "").trim());
                    }
                }

                if (startPos < 0 || endPos < 0 || endPos >= fileSize || startPos > endPos) {
                    // 要求的范围不满足
                    bo.getRes().setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                    return;
                }
                // 断点续传 状态码206
                bo.getRes().setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            } catch (NumberFormatException e) {
                logger.error(e.getMessage());
                startPos = 0;
            }
        }
        // 总共需要读取的字节
        long rangLength = endPos - startPos + 1;
        logger.info("……………………rangLength……………………" + rangLength);
        bo.getRes().setHeader("Content-Range", String.format("bytes %d-%d/%d", startPos, endPos, fileSize));
        bo.getRes().addHeader("Content-Length", String.valueOf(rangLength));
        bo.getRes().addHeader("Content-Type", "video/mp4");
        try (BufferedOutputStream bos = new BufferedOutputStream(bo.getRes().getOutputStream());
             BufferedInputStream bis = new BufferedInputStream(
                     minioFileUtil.getObject(bucketName, bo.getFileName(), startPos, rangLength))) {
            IOUtils.copy(bis, bos);
        } catch (
                IOException e) {
            if (e instanceof ClientAbortException) {
                // ignore 这里不打印日志，这里的异常原因是用户在拖拽视频进度造成的
            } else {
                logger.error(e.getMessage());
            }
        }
    }
}
